package lab.poc.erl.otp;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;

import lab.poc.erl.Actor;
import lab.poc.erl.Match;
import lab.poc.erl.Node;
import lab.poc.erl.Pattern;
import lab.poc.erl.PatternFrom;
import lab.poc.erl.Patterns;
import lab.poc.erl.Pid;
import lab.poc.erl.Reference;

/**
 * GenServer builds a function call abstraction on top of the message passing
 * protocol, while at the same time synchronizing access to a resource by
 * serializing requests (thus "abstracting out" the concurrency).
 */
public abstract class GenServer implements GenServerBehavior {

	private static final long DEFAULT_TIMEOUT = 5000L;

	protected Pid actorPid = null;

	public static final <T extends GenServer> T startLink(
			final Class<T> genServerClass, final String alias,
			final List<Object> args) {
		return startLink(genServerClass, alias, args, DEFAULT_TIMEOUT);
	}

	public static final <T extends GenServer> T startLink(
			final Class<T> genServerClass, final String alias,
			final List<Object> args, final long timeout) {
		T genServer = null;
		try {
			Constructor<T> genServerCons = genServerClass.getConstructor();
			genServerCons.setAccessible(true);
			genServer = genServerCons.newInstance();
		} catch (InstantiationException e) {
			throw new TypeNotPresentException(genServerClass.getName(), e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(genServerClass.getName(), e);
		} catch (NoSuchMethodException e) {
			throw new TypeNotPresentException(genServerClass.getName()
					+ ".<init>", e);
		} catch (IllegalAccessException e) {
			throw new SecurityException(genServerClass.getName(), e);
		}

		genServer.startLink(alias, args, timeout);
		return genServer;
	}

	protected GenServer() {
	}

	protected final void startLink(final String alias, final List<Object> args, final long timeout) {
		Node node = Node.getNode();

		int newArgsCapacity = 2;
		if (args != null && !args.isEmpty()) {
			newArgsCapacity += args.size();
		}
		ArrayList<Object> args_ = new ArrayList<Object>(newArgsCapacity);
		args_.add(this);
		Reference ref = node.makeRef();
		args_.add(ref);
		if (newArgsCapacity > 2) {
			args_.addAll(args);
		}

		Pid pid = node.spawnLink(GenServerActor.class, args_);
		// wait until the thread is running and init() has been executed
		try {
			Match m = Actor.acceptWithTimeout(timeout, new UpAndRunning(pid, ref, true));
			if (!((UpAndRunning) m.msg).initSucceeded) {
				throw new StartException("initialization didn't succeed");
			}
		} catch (TimeoutException e) {
			throw new StartException("timed out", e);
		}
		try {
			node.register(alias, pid);
		} catch (IllegalArgumentException e) {
			pid.exit();
			throw e;
		}
		actorPid = pid;
	}

	public static final Object call(final String alias, final Object message) {
		return call(alias, message, DEFAULT_TIMEOUT);
	}

	public static final Object call(final String alias, final Object message, final long timeout) {
		return call_(getActorPid(alias), message, timeout);
	}

	public static final void cast(final String alias, final Object message) {
		cast_(getActorPid(alias), message);
	}

	private static final Pid getActorPid(final String alias) {
		final Pid actorPid = Node.getNode().whereIs(alias);
		if (Pid.UNDEFINED.equals(actorPid)) {
			throw new IllegalArgumentException(alias + " is not registered");
		}
		return actorPid;
	}

	protected final Object call(final Object message) {
		return call(message, DEFAULT_TIMEOUT);
	}

	protected final Object call(final Object message, final long timeout) {
		return call_(actorPid, message, timeout);
	}

	protected final void cast(final Object message) {
		cast_(actorPid, message);
	}

	private static final void cast_(final Pid receiver, final Object msg) {
		receiver.send(new AsyncCastMessage(msg));
	}

	private static final Object call_(final Pid receiver, final Object msg, final long timeout) {
		Reference ref = Node.getNode().makeRef();
		SyncCallMessage msg_ = new SyncCallMessage(msg, ref);
		receiver.send(msg_);
		SyncCallReplyPattern replyPattern = new SyncCallReplyPattern(receiver, ref);
		Match m = null;
		try {
			m = Actor.acceptWithTimeout(timeout, replyPattern);
		} catch (TimeoutException e) {
			throw new NoTimelyResponseException("callee didn't respond within "
					+ timeout + " ms", e);
		}
		return ((SyncCallMessage) m.msg).msg;
	}

	private static final class GenServerActor extends Actor {

		private GenServerBehavior this_;
		private Object currState;

		@SuppressWarnings("unused")
		public GenServerActor() {
		}

		@Override
		protected final void onSpawn(final List<Object> args) {
			this_ = (GenServerBehavior) args.get(0);
			Reference ref = (Reference) args.get(1);
			List<Object> args_ = null;
			if (args.size() > 2) {
				args_ = args.subList(2, args.size() - 1);
			}
			GenServerOK ok = this_.otpInit(args_);
			if (ok == null) {
				this_.otpTerminate(null, StopReason.ABNORMAL,
						"init() returned null", new IllegalArgumentException());
				sendUpAndRunning(new UpAndRunning(self(), ref, false));
				throw new StartException("init() returned null",
						new IllegalArgumentException());
			}
			if (ok.isStop()) {
				GenServerOKStop okStop = (GenServerOKStop) ok;
				this_.otpTerminate(okStop.newState, okStop.stopType,
						okStop.reasonInfo, okStop.optionalReasonInfo);
				sendUpAndRunning(new UpAndRunning(self(), ref, false));
				throw new StartException("init() returned GenServerOKStop: "
						+ okStop.reasonInfo, okStop.optionalReasonInfo);
			}
			currState = ok.newState;

			sendUpAndRunning(new UpAndRunning(self(), ref, true));

			loop();
		}

		private void loop() {
			boolean accept = true;
			while (accept) {
				final Match m = receive(AsyncCastRequestPattern.P,
						SyncCallRequestPattern.P, Patterns.AnyMsg);

				switch (m.pat.<RequestKind> type()) {
				case SYNC: {
					SyncCallMessage msg = (SyncCallMessage) m.msg;
					GenServerReply reply = this_.otpHandleCall(msg.msg, currState, m.from, msg.ref);
					if (reply != null) {
						if (reply.isStop()) {
							accept = false;
							GenServerReplyStop stop = (GenServerReplyStop) reply;
							currState = stop.newState;
							this_.otpTerminate(stop.newState, stop.stopType,
									stop.reasonInfo, stop.optionalReasonInfo);
							msg.msg = stop.replyMsg; // reuse message object
							sendReply(m.from, msg);
							handleStopReason(stop.stopType, stop.reasonInfo, stop.optionalReasonInfo);
						} else {
							currState = reply.newState;
							msg.msg = reply.replyMsg; // reuse message object
							sendReply(m.from, msg);
						}
					} else { // null
						accept = false;
						handleNull("handleCall()");
					}
					break;
				} // case SYNC
				case ASYNC: {
					AsyncCastMessage msg = (AsyncCastMessage) m.msg;
					GenServerOK ok = this_.otpHandleCast(msg.msg, currState, m.from);
					if (ok != null) {
						if (ok.isStop()) {
							accept = false;
							processStop((GenServerOKStop) ok);
						} else {
							currState = ok.newState;
						}
					} else { // null
						accept = false;
						handleNull("handleCast()");
					}
					break;
				} // case ASYNC
				default: {
					GenServerOK ok = this_.otpHandleUnknown(m.msg, currState, m.from);
					if (ok != null) {
						if (ok.isStop()) {
							accept = false;
							processStop((GenServerOKStop) ok);
						} else {
							currState = ok.newState;
						}
					} else { // null
						accept = false;
						handleNull("handleUnknown()");
					}
				} // default
				} // switch(type)
			}
		}

		private void handleNull(final String method) {
			this_.otpTerminate(currState, StopReason.ABNORMAL, method
					+ " returned null", new IllegalArgumentException());
			this.exit(); // actually not necessary
			throw new GenServerException("aborted - " + method
					+ " returned null", new IllegalArgumentException());
		}

		private void processStop(final GenServerOKStop stop) {
			currState = stop.newState;
			this_.otpTerminate(stop.newState, stop.stopType,
					stop.reasonInfo, stop.optionalReasonInfo);
			handleStopReason(stop.stopType, stop.reasonInfo, stop.optionalReasonInfo);
		}

		private void handleStopReason(final StopReason reason,
				final String reasonInfo, final Throwable optionalReasonInfo) {
			switch (reason) {
			case ABNORMAL:
				throw new GenServerException("aborted - " + reasonInfo,
						optionalReasonInfo);
			case CANCEL:
				this.exit(); // actually not necessary
				break;
			case NORMAL:
				// run to completion
			}
		}

		private static void sendReply(final Pid caller, final SyncCallMessage reply) {
			caller.send(reply);
		}

		private void sendUpAndRunning(final UpAndRunning up) {
			final Pid parent = parent();
			if (parent != null) {
				parent.send(up);
			}
		}

		private static enum RequestKind {
			ASYNC,
			SYNC,
			UNKNOWN
		}

		private static final class SyncCallRequestPattern implements Pattern {
			static final SyncCallRequestPattern P = new SyncCallRequestPattern();

			@Override
			public boolean approve(final Object msg) {
				return msg instanceof SyncCallMessage;
			}

			@Override
			@SuppressWarnings("unchecked")
			public RequestKind type() {
				return RequestKind.SYNC;
			}
		}

		private static final class AsyncCastRequestPattern implements Pattern {
			static final AsyncCastRequestPattern P = new AsyncCastRequestPattern();

			@Override
			public boolean approve(final Object msg) {
				return msg instanceof AsyncCastMessage;
			}

			@Override
			@SuppressWarnings("unchecked")
			public RequestKind type() {
				return RequestKind.ASYNC;
			}
		}
	}

	// used for both: request and response
	private static final class SyncCallMessage {
		private volatile Object msg;
		private final Reference ref;
		SyncCallMessage(final Object msg, final Reference ref) {
			this.msg = msg;
			this.ref = ref;
		}
	}

	private static final class AsyncCastMessage {
		private final Object msg;
		AsyncCastMessage(final Object msg) {
			this.msg = msg;
		}
	}

	private static final class SyncCallReplyPattern extends PatternFrom {
		private final Reference ref;

		SyncCallReplyPattern(final Pid receiveFrom, final Reference ref) {
			super(receiveFrom);
			this.ref = ref;
		}

		@Override
		public boolean approve(final Object msg) {
			if (msg instanceof SyncCallMessage) {
				if (ref.equals(((SyncCallMessage) msg).ref)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public <E extends Enum<E>> E type() { // unused
			return null;
		}
	}

	private static final class UpAndRunning extends PatternFrom {
		private final Reference ref;
		private final boolean initSucceeded;

		UpAndRunning(final Pid receiveFrom, final Reference uniqueId,
				final boolean initSucceeded) {
			super(receiveFrom);
			this.ref = uniqueId;
			this.initSucceeded = initSucceeded;
		}

		@Override
		public final boolean approve(final Object msg) {
			if (msg instanceof UpAndRunning) {
				if (ref.equals(((UpAndRunning) msg).ref)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public final <E extends Enum<E>> E type() { // unused
			return null;
		}
	}
}
